﻿namespace WebCamTimeLapse
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.IO;
    using System.Xml;
    using System.Web;
    using System.Threading;

    public delegate void ImageCaptureHandler(string message);

    /// <summary>
    /// The Capture class is the brain that keeps the list of web cams and captures images from cams
    /// that are overdue.
    /// </summary>
    class Capture
    {
        private List<WebCam> webCams = new List<WebCam>();

        /// <summary>
        /// This event is raised whenever an image is captured from a web cam.
        /// </summary>
        public event ImageCaptureHandler ImageCaptureEvent;

        /// <summary>
        /// Constructor.
        /// </summary>
        public Capture()
        {
            LoadSetup();
        }

        /// <summary>
        /// Call this before you exit.
        /// </summary>
        public void CloseApp()
        {
            this.SaveSetup();
        }

        /// <summary>
        /// Add a new web cam to the list.
        /// </summary>
        /// <param name="name">Description</param>
        /// <param name="url">URL for the image</param>
        /// <param name="interval">Number of minutes between image captures</param>
        /// <param name="localDir">Local directory where the images should be saved</param>
        public void Add(string name, string url, int interval, string localDir)
        {
            WebCam wc = new WebCam(name, url, interval, localDir);
            webCams.Add(wc);
            SaveSetup();
        }

        /// <summary>
        /// Remove the web cam at the given index.
        /// </summary>
        /// <param name="index">0 based index</param>
        public void RemoveAt(int index)
        {
            webCams.RemoveAt(index);
            SaveSetup();
        }

        /// <summary>
        /// Returns a list of all the web cam descriptions.
        /// </summary>
        /// <returns></returns>
        public List<string> GetCamNames()
        {
            List<string> result = new List<string>();
            foreach (WebCam wc in this.webCams)
            {
                result.Add(wc.Name);
            }
            return result;
        }

        /// <summary>
        /// This is called every clock tick (every minute.) It loops through every web cam. If the web
        /// cam needs it's picture taken, a thread is spawned to perform that action.
        /// </summary>
        public void Tick()
        {
            foreach (WebCam wc in webCams)
            {
                if (wc.NextCapture < DateTime.Now)
                {
                    ThreadPool.QueueUserWorkItem(new WaitCallback(CaptureImage), wc);
                }
            }
        }
        
        /// <summary>
        /// Create a web request to download the image and save it to the local path. The filename is 
        /// the current date ticks so that it is always unique and gives an ordering.
        /// </summary>
        /// <param name="obj"></param>
        private void CaptureImage(object obj)
        {
            WebCam wc = (WebCam)obj;
            FileStream fs = null;
            Stream responseStream = null;

            try
            {
                wc.LastCapture = DateTime.Now;

                // a bogus parameter is appended to the string so that we don't hit caching issues
                System.Net.WebRequest request = System.Net.WebRequest.Create(wc.URL + "?a=" + DateTime.Now.Ticks.ToString());
                System.Net.WebResponse response = request.GetResponse();
                responseStream = response.GetResponseStream();

                string filename = Path.Combine(wc.LocalDirectory, DateTime.Now.Ticks.ToString() + ".jpg");

                // make sure the directory exists
                if (Directory.Exists(wc.LocalDirectory) == false)
                {
                    Directory.CreateDirectory(wc.LocalDirectory);
                }

                fs = new FileStream(filename, FileMode.Create);

                int b = -1;
                while ((b = responseStream.ReadByte()) != -1)
                {
                    fs.WriteByte((byte)b);
                }

                OnCapture(wc.Name, filename);
            }
            catch (Exception e)
            {
                OnCapture(wc.Name, e.Message);
            }
            finally
            {
                if (responseStream != null) responseStream.Close();
                if (fs != null) fs.Close();
            }
        }

        /// <summary>
        /// Fire the OnCapture event. The message says which cam fired, what time it fired, and where the image was saved.
        /// It can also contain an error message if there was a problem.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="filename"></param>
        protected void OnCapture(string name, string filename)
        {
            if (ImageCaptureEvent != null)
            {
                string message = String.Format("{0}: {1} - {2}", DateTime.Now.ToString(), name, filename);
                ImageCaptureEvent(message);
            }
        }

        /// <summary>
        /// Save the current list of web cams as an xml document. The file is saved to usersettings.xml in the
        /// current directory.
        /// </summary>
        private void SaveSetup()
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml("<cams></cams>");

            foreach (WebCam wc in webCams)
            {
                XmlNode camNode = xmlDoc.CreateElement("cam");
                camNode.AppendChild(CreateNode(xmlDoc, "name", wc.Name));
                camNode.AppendChild(CreateNode(xmlDoc, "interval", wc.Interval.ToString()));
                camNode.AppendChild(CreateNode(xmlDoc, "localdirectory", wc.LocalDirectory));
                camNode.AppendChild(CreateNode(xmlDoc, "lastcapture", wc.LastCapture.ToString()));
                camNode.AppendChild(CreateNode(xmlDoc, "url", wc.URL));
                xmlDoc.DocumentElement.AppendChild(camNode);
            }

            xmlDoc.Save("usersettings.xml");
        }

        /// <summary>
        /// This a helper method for the SaveSetup method. It creates a new node in the XML document.
        /// </summary>
        /// <param name="xmlDoc"></param>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private XmlNode CreateNode(XmlDocument xmlDoc, string name, string value)
        {
            XmlNode node = xmlDoc.CreateElement(name);
            node.InnerText = HttpUtility.HtmlEncode(value);
            return node;
        }

        /// <summary>
        /// Read the usersettings.xml file (if it exists) and recreate the state of the app when it was last used.
        /// </summary>
        private void LoadSetup()
        {
            if (File.Exists("usersettings.xml"))
            {
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load("usersettings.xml");
                XmlNodeList list = xmlDoc.SelectNodes("/cams/cam");

                foreach (XmlNode node in list)
                {
                    string name = GetSettingsValue(node, "name");
                    string interval = GetSettingsValue(node, "interval");
                    string localdirectory = GetSettingsValue(node, "localdirectory");
                    string url = GetSettingsValue(node, "url");
                    string lastCapture = GetSettingsValue(node, "lastcapture");

                    WebCam wc = new WebCam(name, url, Int32.Parse(interval), localdirectory);
                    wc.LastCapture = DateTime.Parse(lastCapture);
                    this.webCams.Add(wc);
                }
            }
        }

        /// <summary>
        /// Helper method for LoadSetup. It finds the specified child node and decodes the text.
        /// </summary>
        /// <param name="node"></param>
        /// <param name="childName"></param>
        /// <returns></returns>
        private string GetSettingsValue(XmlNode node, string childName)
        {
            string text = node.SelectSingleNode(childName).InnerText;
            text = HttpUtility.HtmlDecode(text);
            return text;
        }

    }
}
